Skip to contentMethod: static {...}
1: /*
2: * *************************************************************************************************************************************************************
3: *
4: * SteelBlue: DCI User Interfaces
5: * http://tidalwave.it/projects/steelblue
6: *
7: * Copyright (C) 2015 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *************************************************************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12: * You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17: * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18: *
19: * *************************************************************************************************************************************************************
20: *
21: * git clone https://bitbucket.org/tidalwave/steelblue-src
22: * git clone https://github.com/tidalwave-it/steelblue-src
23: *
24: * *************************************************************************************************************************************************************
25: */
26: package it.tidalwave.ui.javafx.impl.util;
27:
28: import java.lang.ref.WeakReference;
29: import java.lang.reflect.InvocationTargetException;
30: import java.lang.reflect.Proxy;
31: import jakarta.annotation.Nonnull;
32: import java.util.HashMap;
33: import java.util.Map;
34: import java.util.concurrent.CountDownLatch;
35: import java.util.concurrent.Executor;
36: import java.util.concurrent.atomic.AtomicReference;
37: import javafx.fxml.FXML;
38: import javafx.fxml.FXMLLoader;
39: import javafx.application.Platform;
40: import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
41: import it.tidalwave.ui.javafx.JavaFXBinder;
42: import it.tidalwave.ui.javafx.JavaFXMenuBarControl;
43: import it.tidalwave.ui.javafx.JavaFXToolBarControl;
44: import it.tidalwave.ui.javafx.impl.DefaultJavaFXBinder;
45: import it.tidalwave.ui.javafx.impl.DefaultJavaFXMenuBarControl;
46: import it.tidalwave.ui.javafx.impl.DefaultJavaFXToolBarControl;
47: import it.tidalwave.util.PreferencesHandler;
48: import it.tidalwave.util.ReflectionUtils;
49: import lombok.Getter;
50: import lombok.RequiredArgsConstructor;
51: import lombok.extern.slf4j.Slf4j;
52: import static lombok.AccessLevel.PRIVATE;
53:
54: /***************************************************************************************************************************************************************
55: *
56: * @stereotype Factory
57: *
58: * @author Fabrizio Giudici
59: *
60: **************************************************************************************************************************************************************/
61: @RequiredArgsConstructor(access = PRIVATE) @Slf4j
62: public final class JavaFXSafeComponentBuilder<I, T extends I>
63: {
64: public static final Map<Class<?>, Object> BEANS = new HashMap<>();
65:
66: @Getter
67: private static final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
68:
69: @Getter
70: private static final JavaFXBinder javaFxBinder = new DefaultJavaFXBinder(executor);
71:
72: @Getter
73: private static final JavaFXToolBarControl toolBarControl = new DefaultJavaFXToolBarControl();
74:
75: @Getter
76: private static final JavaFXMenuBarControl menuBarControl = new DefaultJavaFXMenuBarControl();
77:
78: @Nonnull
79: private final Class<T> componentClass;
80:
81: @Nonnull
82: private final Class<I> interfaceClass;
83:
84: private WeakReference<T> presentationRef = new WeakReference<>(null);
85:
86: static
87: {
88: executor.setWaitForTasksToCompleteOnShutdown(false);
89: executor.setThreadNamePrefix("javafxBinder-");
90: // Fix for STB-26
91: executor.setCorePoolSize(1);
92: executor.setMaxPoolSize(1);
93: executor.setQueueCapacity(10000);
94: BEANS.put(JavaFXBinder.class, javaFxBinder);
95: BEANS.put(Executor.class, executor);
96: BEANS.put(JavaFXToolBarControl.class, toolBarControl);
97: BEANS.put(JavaFXMenuBarControl.class, menuBarControl);
98: BEANS.put(PreferencesHandler.class, PreferencesHandler.getInstance());
99: }
100:
101: @Nonnull
102: public static <J, X extends J> JavaFXSafeComponentBuilder<J, X> builderFor (@Nonnull final Class<X> componentClass)
103: {
104: final var interfaceClass = (Class<J>)componentClass.getInterfaces()[0]; // FIXME: guess
105: return new JavaFXSafeComponentBuilder<>(componentClass, interfaceClass);
106: }
107:
108: @Nonnull
109: public static <J, X extends J> JavaFXSafeComponentBuilder<J, X> builderFor (@Nonnull final Class<J> interfaceClass,
110: @Nonnull final Class<X> componentClass)
111: {
112: return new JavaFXSafeComponentBuilder<>(componentClass, interfaceClass);
113: }
114:
115: /***********************************************************************************************************************************************************
116: * Creates an instance of a surrogate JavaFX delegate. JavaFX delegates (controllers in JavaFX jargon) are those
117: * objects with fields annotated with {@link @FXML} that are created by the {@link FXMLLoader} starting from a
118: * {@code .fxml} file. Sometimes a surrogate delegate is needed, that is a class that is not mapped to any
119: * {@link @FXML} file, but whose fields are copied from another existing delegate.
120: *
121: * @param componentClass the class of the surrogate
122: * @param fxmlFieldsSource the existing JavaFX delegate with {@code @FXML} annotated fields.
123: * @return the new surrogate delegate
124: **********************************************************************************************************************************************************/
125: @Nonnull
126: public static <J, X extends J> X createInstance (@Nonnull final Class<X> componentClass,
127: @Nonnull final Object fxmlFieldsSource)
128: {
129: final JavaFXSafeComponentBuilder<J, X> builder = builderFor(componentClass);
130: return builder.createInstance(fxmlFieldsSource);
131: }
132:
133: /***********************************************************************************************************************************************************
134: * Creates an instance of a surrogate JavaFX delegate. JavaFX delegates (controllers in JavaFX jargon) are those
135: * objects with fields annotated with {@link @FXML} that are created by the {@link FXMLLoader} starting from a
136: * {@code .fxml} file. Sometimes a surrogate delegate is needed, that is a class that is not mapped to any
137: * {@link @FXML} file, but whose fields are copied from another existing delegate.
138: *
139: * @param fxmlFieldsSource the existing JavaFX delegate with {@code @FXML} annotated fields.
140: * @return the new surrogate delegate
141: **********************************************************************************************************************************************************/
142: @Nonnull
143: public synchronized T createInstance (@Nonnull final Object fxmlFieldsSource)
144: {
145: log.trace("createInstance({})", fxmlFieldsSource);
146: var presentation = presentationRef.get();
147:
148: if (presentation == null)
149: {
150: presentation = Platform.isFxApplicationThread() ? createComponentInstance() : createComponentInstanceInJAT();
151: copyFxmlFields(presentation, fxmlFieldsSource); // FIXME: in JFX thread?
152:
153: try // FIXME // FIXME: in JFX thread?
154: {
155: presentation.getClass().getDeclaredMethod("initialize").invoke(presentation);
156: }
157: catch (NoSuchMethodException | SecurityException | IllegalAccessException
158: | InvocationTargetException e)
159: {
160: log.warn("No postconstruct in {}", presentation);
161: }
162:
163: presentation = (T)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
164: new Class[] { interfaceClass },
165: new JavaFXSafeProxy(presentation));
166: presentationRef = new WeakReference<>(presentation);
167: }
168:
169: return presentation;
170: }
171:
172: /***********************************************************************************************************************************************************
173: *
174: **********************************************************************************************************************************************************/
175: @Nonnull
176: private T createComponentInstance()
177: {
178: return ReflectionUtils.instantiateWithDependencies(componentClass, BEANS);
179: }
180:
181: /***********************************************************************************************************************************************************
182: *
183: **********************************************************************************************************************************************************/
184: @Nonnull
185: private T createComponentInstanceInJAT()
186: {
187: final var reference = new AtomicReference<T>();
188: final var countDownLatch = new CountDownLatch(1);
189:
190: Platform.runLater(() ->
191: {
192: reference.set(createComponentInstance());
193: countDownLatch.countDown();
194: });
195:
196: try
197: {
198: countDownLatch.await();
199: }
200: catch (InterruptedException e)
201: {
202: log.error("", e);
203: throw new RuntimeException(e);
204: }
205:
206: return reference.get();
207: }
208:
209: /***********************************************************************************************************************************************************
210: * Inject fields annotated with {@link FXML} in {@code source} to {@code target}.
211: *
212: * @param target the target object
213: * @param source the source object
214: **********************************************************************************************************************************************************/
215: private void copyFxmlFields (@Nonnull final Object target, @Nonnull final Object source)
216: {
217: log.debug("injecting {} with fields from {}", target, source);
218: final Map<String, Object> valuesMapByFieldName = new HashMap<>();
219:
220: for (final var field : source.getClass().getDeclaredFields())
221: {
222: if (field.getAnnotation(FXML.class) != null)
223: {
224: final var name = field.getName();
225:
226: try
227: {
228: field.setAccessible(true);
229: final var value = field.get(source);
230: valuesMapByFieldName.put(name, value);
231: log.trace(">>>> available field {}: {}", name, value);
232: }
233: catch (IllegalArgumentException | IllegalAccessException e)
234: {
235: throw new RuntimeException("Cannot read field " + name + " from " + source, e);
236: }
237: }
238: }
239:
240: for (final var field : target.getClass().getDeclaredFields())
241: {
242: final var fxml = field.getAnnotation(FXML.class);
243:
244: if (fxml != null)
245: {
246: final var name = field.getName();
247: final var value = valuesMapByFieldName.get(name);
248:
249: if (value == null)
250: {
251: throw new RuntimeException("Can't inject " + name + ": available: " + valuesMapByFieldName.keySet());
252: }
253:
254: field.setAccessible(true);
255:
256: try
257: {
258: field.set(target, value);
259: }
260: catch (IllegalArgumentException | IllegalAccessException e)
261: {
262: throw new RuntimeException("Cannot inject field " + name + " to " + target, e);
263: }
264: }
265: }
266:
267: ReflectionUtils.injectDependencies(target, BEANS);
268: }
269: }